package com.young.ums.web;

import com.young.common.client.ums.UmsClientHelper;
import com.young.common.core.cache.ICacheClient;
import com.young.common.exception.BaseException;
import com.young.common.util.*;
import com.young.interfaces.log.annotation.Log;
import com.young.interfaces.ums.model.SsoVerifyDTO;
import com.young.ums.model.*;
import com.young.ums.service.*;
import com.young.ums.util.SecurityCodeCreater;
import com.young.ums.util.UmsConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 单点登录服务
 * @author imrookie
 * @date 2018/9/15
 */
@Controller
@RequestMapping(value = "/sso", method = {RequestMethod.GET, RequestMethod.POST})
public class SsoController {

    private static final Logger logger = LoggerFactory.getLogger(SsoController.class);
    /**
     * 配置服务
     */
    @Resource(name="umsConfigService")
    IUmsConfigService configService;
    /**
     * 会话服务
     */
    @Resource(name="ssoSessionService")
    ISsoSessionService ssoSessionService;
    /**
     * 登录日志
     */
    @Resource(name="loginLogService")
    ILoginLogService loginLogService;
    /**
     * sso服务
     */
    @Resource
    ISsoService ssoService;
    /**
     * ums缓存
     */
    @Autowired
    IUmsCacheService umsCacheService;

    /**
     * 刷新验证码
     * @param sessionId
     * @param request
     * @param response
     * @return
     */
    @Log("刷新验证码")
    @RequestMapping(value = "/securityCode/{sessionId}", method = RequestMethod.GET)
    public void securityCode(@PathVariable("sessionId")String sessionId, HttpServletRequest request,HttpServletResponse response) {
        if (StringUtils.isBlank(sessionId)) {
            //安全认证会话id无效,请刷新页面重新登录
            logger.warn("[刷新验证码] sessionId为空");
            try {
                response.getWriter().write("安全认证失败,请刷新页面重新登录!");
            } catch (IOException e) {
                logger.error("[刷新验证码] 获取writer异常:", e);
            }
            return;
        }
        //SsoSecure ssoSecure = SsoSecureCache.get(sessionId);
        SsoSecure ssoSecure = umsCacheService.getSsoSecure(sessionId);
        if (ssoSecure == null) {
            //安全认证会话id无效,请刷新页面重新登录
            logger.warn("[刷新验证码] 根据sessionId找不到对应的安全认证,sessionId={}", sessionId);
            try {
                response.getWriter().write("安全认证失败,请刷新页面重新登录!");
            } catch (IOException e) {
                logger.error("[刷新验证码] 获取writer异常:", e);
            }
            return;
        }
        //生成验证码图片,并更新安全认证中的验证码信息
        SecurityCodeCreater.SecurityCodeImage securityCodeImage = SecurityCodeCreater.createSecurityCodeImage();
        ssoSecure.setSecurityCode(securityCodeImage.getCode());
        ssoSecure.setSecurityCodeCreateTime(securityCodeImage.getCreateTime());

        //更新缓存
        umsCacheService.setSsoSecure(sessionId, ssoSecure);

        //写入图片
        OutputStream os = null;
        try {
            os = response.getOutputStream();
        } catch (IOException e) {
            logger.error("[刷新验证码] 获取响应流异常:", e);
        }
        try {
            ImageIO.write(securityCodeImage.getImage(), "jpeg", os);
        } catch (IOException e) {
            logger.error("[刷新验证码] 将验证码图片写入响应流异常:", e);
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    logger.error("[刷新验证码] 关闭响应流异常:", e);
                }
            }
        }
    }

    /**
     * 安全认证(在登录前应先请求此接口拿到安全相关参数)
     * @param request
     * @param response
     * @return
     */
    @Log("安全认证")
    @RequestMapping(value = "/secured", method = RequestMethod.POST)
    @ResponseBody
    public Object secured(HttpServletRequest request,HttpServletResponse response) {
        SsoSecure ssoSecure = new SsoSecure();
        ssoSecure.setSessionId(CommonUtil.getUUID());
        ssoSecure.setSecuredTime(new Date());
        //SsoSecureCache.put(ssoSecure.getSessionId(), ssoSecure);//插入缓存
        umsCacheService.setSsoSecure(ssoSecure.getSessionId(), ssoSecure);//插入缓存
        return Result.buildSuccess(ssoSecure);
    }
    /**
     * 登录-post方式
     * @param request
     * @param response
     * @return
     */
    @Log("用户登录")
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public Object login(HttpServletRequest request,HttpServletResponse response) {
        String account = request.getParameter("account");//用户名
        String pwd = request.getParameter("password");//密码
        String securityCode = request.getParameter("securityCode");//验证码
        String sessionId = request.getParameter("sessionId");//安全认证(会话)id
        String clientIp = ClientInfoUtil.getClientIpAddr(request);//客户端ip地址
        try{
            String token = null;//令牌
            com.young.interfaces.ums.model.User loginUser = UmsClientHelper.getLoginUser();//已登陆用户
            if (loginUser != null) {
                token = loginUser.getToken();
                logger.info("[SSO单点登录] 检测到该会话内已存在登录用户{},跳过验证！", loginUser);
            } else {
                //参数验证
                if (StringUtils.isBlank(account) || StringUtils.isBlank(pwd)) {
                    logger.info("[SSO单点登录] 用户名或密码无效,account={},password={}", account, pwd);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "用户名或密码无效", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("用户名或密码无效");
                }
                if (StringUtils.isBlank(securityCode)) {
                    logger.info("[SSO单点登录] 验证码为空,account={},password={}", account, pwd);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "验证码为空", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("验证码为空");
                }
                if (StringUtils.isBlank(sessionId)) {
                    logger.info("[SSO单点登录] 安全认证编号为空,account={},password={}", account, pwd);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "安全认证编号为空", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("安全认证编号为空");
                }

                //SsoSecure ssoSecure = SsoSecureCache.get(sessionId);//安全认证
                SsoSecure ssoSecure = umsCacheService.getSsoSecure(sessionId);//安全认证
                if (ssoSecure == null) {
                    logger.info("[SSO单点登录] 无效的安全认证,account={},password={},sessionId={}", account, pwd, sessionId);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "无效的安全认证", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("无效的安全认证,请刷新登录页重新登录");
                }
                if (StringUtils.isBlank(ssoSecure.getSecurityCode()) || ssoSecure.getSecurityCodeCreateTime() == null
                        || new Date().getTime() - ssoSecure.getSecurityCodeCreateTime().getTime() > 1000L * 60 * 5) {//验证码5分钟有效
                    logger.info("[SSO单点登录] 验证码无效,account={},password={},sessionId={}", account, pwd, sessionId);
                    ssoSecure.setSecurityCode(null);//服务端强制清空验证码,保证一个验证码只使用1次
                    ssoSecure.setSecurityCodeCreateTime(null);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "验证码无效", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("验证码无效,请刷新验证码");
                }
                if (!securityCode.equalsIgnoreCase(ssoSecure.getSecurityCode())) {//不区分大小写
                    logger.info("[SSO单点登录] 验证码错误,account={},password={},sessionId={},用户输入的验证码={},正确验证码={}", account, pwd, sessionId, securityCode, ssoSecure.getSecurityCode());
                    ssoSecure.setSecurityCode(null);//服务端强制清空验证码,保证一个验证码只使用1次
                    ssoSecure.setSecurityCodeCreateTime(null);
                    writeLoginLog(account, UmsConstants.LoginLogResult.NO, "验证码错误", clientIp, null);//记录登录日志
                    return SsoResult.buildFail("验证码错误");
                }

                //解密账号和密码,先rsa解密,再base64解码
                logger.info("[SSO单点登录] 用户名/密码解密前,account={},password={}", account, pwd);
                account = this.decryptAccountOrPassword(account);//解密账号
                pwd = this.decryptAccountOrPassword(pwd);//解密密码
                logger.info("[SSO单点登录] 用户名/密码解密后,account={},password={}", account, pwd);
                //TODO 临时方案
                //设备
                String agent = request.getHeader("User-Agent");
                if (agent != null && (agent.indexOf("Android") > -1 || agent.indexOf("android") > -1)){
                    agent = "ANDROID";
                }else if (agent != null && (agent.indexOf("iPhone") > -1 || agent.indexOf("iPad") > -1 || agent.indexOf("ios") > -1)){
                    agent = "IOS";
                }else if (agent != null && (agent.indexOf("windowsphone") > -1)){
                    agent = "WINDOWSPHONE";
                }else{
                    agent = "H5";
                }

                //sso 核心方法
                token = ssoService.sso(account, pwd, clientIp, agent);

                //写入cookie
                Cookie cookie = new Cookie(UmsConstants.SSO.TOKEN_KEY, token);
                String domain = configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_COOKIE_DOMAIN);
                if (StringUtils.isNotBlank(domain)){
                    cookie.setDomain(domain);//设置cookie的域,用于前台cookie共享
                }
                cookie.setPath("/");
                response.addCookie(cookie);

            }
            /**
             * 获取重定向页面
             * 1.从redirectTo参数获取;
             * 2.取用户的目的地址(fromUrl);
             * 3.默认跳转首页;
             */
            String redirectTo = request.getParameter("redirectTo");
            if (StringUtils.isBlank(redirectTo)) {
                redirectTo = request.getParameter("fromUrl");
                logger.debug("[SSO单点登录] 入参的重定向地址参数为空,试图取用户的真实目的地址fromUrl,值为:{}", redirectTo);
            }
            //过滤掉注销地址
            if(redirectTo != null && redirectTo.indexOf("logout") > -1){
                logger.info("[SSO单点登录] 过滤掉redirectTo参数:{}", redirectTo);
                redirectTo = "";
            }
            redirectTo = StringUtils.isNotBlank(redirectTo) ? redirectTo : configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_DEFAULT_REDIRECT_URL);

            return SsoResult.buildSuccess(redirectTo, token);
        } catch (BaseException e) {
            logger.error("[SSO单点登录] 登录失败,account={},password={}", account, pwd, e);
            //writeLoginLog(account, UmsConstants.LoginLogResult.NO, e.getInfo(), clientIp, null);//记录登录日志,在sso里面记录了,这里就不再记了
            return SsoResult.buildFail("用户名或密码错误!");
        } catch(Exception e) {
            logger.error("[SSO单点登录] 发生异常,account={},password={}", account, pwd, e);
            writeLoginLog(account, UmsConstants.LoginLogResult.NO, "失败", clientIp, null);//记录登录日志
            return SsoResult.buildFail("登录失败，请联系管理员!");
        }
    }

    /**
     * 解密账号或密码
     * 前端加密过程: 原文 -> utf-8转为byte[] -> base64将byte[]转为byte[] -> rsa公钥加密转为密文
     * 解密过程: 逆运算即可
     * @param str 被加密的账号密码
     * @return
     * @throws Exception
     */
    private String decryptAccountOrPassword(String str) throws Exception {
        byte[] bytes = RSAUtil.decryptByPrivate(Base64.getDecoder().decode(str.getBytes()), UmsConstants.SSO_CLIENT_RSA_KEY_PRIVATE);
        return new String(Base64.getDecoder().decode(bytes), "UTF-8");
    }

    /**
     * 记录登录日志
     * @param account 账号
     * @param result 登录结果,UmsConstants.LoginLogResult
     * @param remark 说明
     * @param ssoSession 登录成功后的会话信息
     */
    private void writeLoginLog(String account, String result, String remark, String clientIp, SsoSession ssoSession){
        try{
            if (UmsConstants.LoginLogResult.NO.equals(result) && StringUtils.isNotBlank(account) && account.length() > 32) {//登录失败,则尝试解密账号,避免密文太长存储不进去
                try {
                    account = this.decryptAccountOrPassword(account);
                } catch (Exception e) {
                    logger.error("[记录登录日志] 登录失败,尝试解密账号后记录日志,解密发生异常,截取账号前32位记录,账号={}", account, e);
                    account = account.substring(0, 32);
                }
            }
            loginLogService.writeLoginLog(account, result, remark, clientIp, ssoSession);
        }catch (Exception e){
            logger.error("[记录登录日志] 发生异常,账号={},登录结果={}", account, result, e);
        }
    }

    /**
     * token验证
     * 20181117将sso下发jwt改为直接下发uuid,故修改此方法
     * 1.获取待验证令牌,从cookie和请求体中获取
     * 2.解析令牌,字符串解析为JWT对象
     * 3.获取服务端保存的令牌信息,验证在服务端是否有效(目前以服务端为准)
     * 4.前面的步骤都成功则返回成功,否则验证失败
     * @param token 令牌,前端参数名为UmsConstants.SSO.TOKEN_KEY
     * @param request
     * @param response
     * @return
     */
    @Log("token验证")
    @RequestMapping(value = "/verifyMyToken", method = {RequestMethod.POST, RequestMethod.GET})
    @ResponseBody
    public Object verifyMyToken(@RequestParam(UmsConstants.SSO.TOKEN_KEY)String token, HttpServletRequest request,HttpServletResponse response) {
        Cookie cookie = CookieUtil.getCookie(request, UmsConstants.SSO.TOKEN_KEY);//从cookie中取令牌
        if (cookie != null && StringUtils.isNotBlank(cookie.getValue())){//cookie存在令牌
            if (StringUtils.isNotBlank(token)){//存在入参token
                if (!token.equals(cookie.getValue())){//cookie和传入的不一致,报错
                    logger.warn("[token验证] cookie与请求体不一致,cookie={},传参={}", cookie.getValue(), token);
                    return Result.buildFail();
                }
            }
            token = cookie.getValue();
        }else if (StringUtils.isBlank(token)){//cookie和入参都为空
            logger.warn("[token验证] token令牌为空:cookie与请求体都为空");
            return Result.buildFail("令牌为空");
        }

        //sso验证
        SsoVerifyDTO ssoVerifyDTO = ssoService.verifySSO(token);

        if (!ssoVerifyDTO.isSuccess()) {
            return Result.buildFail(ssoVerifyDTO.getInfo());
        }

        return Result.buildSuccess();
    }

    /**
     * 注销登录
     * @param request
     * @return
     * @throws Exception
     */
    @Log("注销登录")
    @RequestMapping(value = "/logout", method = RequestMethod.GET)
    public void logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
        com.young.interfaces.ums.model.User user = UmsClientHelper.getLoginUser();
        //重定向到登录页
        String redirectTo = request.getParameter("redirectTo");
        if (StringUtils.isBlank(redirectTo)){
            redirectTo = CommonUtil.getDefaultValue(configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_LOGOUT_REDIRECT_TO), UmsConstants.SSO.REDIRECT_LOGIN);
        }

        /*
        请求最初来自于哪个url
        例如访问a页面,此时由于会话失效而被ums拦截器重定向到注销请求(或者登陆页),重定向时会在链接中携带上链接a,用来在将来登陆成功后直接跳转a页面
        如果是正常退出登陆则没有此参数
         */
        String fromUrl = request.getParameter("fromUrl");
        if (StringUtils.isNotBlank(fromUrl)) {
            redirectTo += redirectTo.indexOf("?") > 0 ? "&" : "?" + "fromUrl=" + URLEncoder.encode(fromUrl, "UTF-8");
        }

        if(user != null){
            logger.info("[注销登录] {}[账号:{}] 退出系统,重定向地址:{}", user.getName(), user.getAccount(), redirectTo);
        }else{
            logger.info("[注销登录] 当前会话不存在用户信息,重定向地址:{}", redirectTo);
        }
        //下线会话信息
        if (user != null && StringUtils.isNotBlank(user.getToken())) {
            ssoSessionService.offLineByToken(user.getToken());
        }

        //清空session
        request.getSession().invalidate();
        //清除token信息
        Cookie cookie = new Cookie(UmsConstants.SSO.TOKEN_KEY, null);
        cookie.setDomain(configService.getUsableConfigValue(UmsConstants.UmsConfigKey.SSO_COOKIE_DOMAIN));
        cookie.setMaxAge(0);
        cookie.setPath("/");
        response.addCookie(cookie);
        response.sendRedirect(redirectTo);
        //return "redirect:" + redirectTo;
    }

    /**
     * 用户注册
     * @param user 用户信息
     * @param request
     * @return
     * @throws Exception
     */
    @Log("用户注册")
    @ResponseBody
    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public Object register(User user, HttpServletRequest request) throws Exception {
        String securityCode = request.getParameter("securityCode");//验证码
        String sessionId = request.getParameter("sessionId");//安全认证(会话)id

        logger.info("[用户注册] 入参user={},验证码={},安全认证id={}", user, securityCode, sessionId);

        if (StringUtils.isBlank(sessionId)) {
            logger.info("[用户注册] 注册失败:安全认证编号为空");
            return SsoResult.buildFail("安全认证编号为空");
        }

        //SsoSecure ssoSecure = SsoSecureCache.get(sessionId);//安全认证
        SsoSecure ssoSecure = umsCacheService.getSsoSecure(sessionId);//安全认证
        if (ssoSecure == null) {
            logger.info("[用户注册] 无效的安全认证,sessionId={}", sessionId);
            return SsoResult.buildFail("无效的安全认证,请刷新页面重新注册");
        }
        if (StringUtils.isBlank(ssoSecure.getSecurityCode()) || ssoSecure.getSecurityCodeCreateTime() == null
                || new Date().getTime() - ssoSecure.getSecurityCodeCreateTime().getTime() > 1000L * 60 * 5) {//验证码5分钟有效
            logger.info("[用户注册] 验证码无效,sessionId={}", sessionId);
            ssoSecure.setSecurityCode(null);//服务端强制清空验证码,保证一个验证码只使用1次
            ssoSecure.setSecurityCodeCreateTime(null);
            return SsoResult.buildFail("验证码无效,请刷新验证码");
        }
        if (!securityCode.equalsIgnoreCase(ssoSecure.getSecurityCode())) {//不区分大小写
            logger.info("[用户注册] 验证码错误,sessionId={},用户输入的验证码={},正确验证码={}", sessionId, securityCode, ssoSecure.getSecurityCode());
            ssoSecure.setSecurityCode(null);//服务端强制清空验证码,保证一个验证码只使用1次
            ssoSecure.setSecurityCodeCreateTime(null);
            return SsoResult.buildFail("验证码错误");
        }
        //注册
        ssoService.register(user);
        return Result.buildSuccess();
    }
}
